package com.wlyuan.index.domain.index.service;


import com.alibaba.fastjson2.JSON;
import com.wlyuan.core.domain.valueobj.WaybillId;
import com.wlyuan.index.domain.index.WaybillIndex;
import com.wlyuan.index.domain.index.repository.WaybillIndexRepository;
import com.wlyuan.index.domain.index.repository.old.WaybillIndexOld;
import com.wlyuan.index.domain.index.repository.old.WaybillIndexOldRepository;
import com.wlyuan.index.domain.index.service.reindex.WaybillReindexService;
import com.wlyuan.index.domain.index.valueobj.WaybillRelation;
import com.wlyuan.index.domain.source.utils.SourceUtils;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
import org.redisson.api.RBucket;
import org.redisson.api.RMapCache;
import org.redisson.api.RSet;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.Serializable;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class WaybillIndexDiffService {
    @Autowired
    private WaybillIndexRepository indexRepository;
    @Autowired
    private WaybillIndexOldRepository indexOldRepository;
    @Autowired
    private WaybillReindexService reindexService;
    @Autowired
    private RedissonClient redissonClient;
    private RMapCache<WaybillId, List<Diff>> diffs;
    private RSet<WaybillId> ignores;
    private RBucket<Integer> page;


    @Data
    @Getter
    @AllArgsConstructor
    public static class Diff implements Serializable {
        private WaybillIndexOld old;
        private WaybillIndexOld sub;
    }


    @Mapper(uses = {SourceUtils.class})
    interface Converter {
        @Mapping(target = "fromAddress", source = "fromAddress.address")
        @Mapping(target = "fromCityCode", source = "fromAddress.cityCode")
        @Mapping(target = "fromCityName", source = "fromAddress.city")
        @Mapping(target = "fromDistrictCode", source = "fromAddress.districtCode")
        @Mapping(target = "fromProvinceCode", source = "fromAddress.provinceCode")
        @Mapping(target = "fromContactName", source = "fromAddress.contactName")
        @Mapping(target = "toAddress", source = "toAddress.address")
        @Mapping(target = "toCityCode", source = "toAddress.cityCode")
        @Mapping(target = "toCityName", source = "toAddress.city")
        @Mapping(target = "toDistrictCode", source = "toAddress.districtCode")
        @Mapping(target = "toProvinceCode", source = "toAddress.provinceCode")
        @Mapping(target = "toContactName", source = "toAddress.contactName")
        @Mapping(target = "deviceCode", source = "trackNo", defaultExpression = "java(\"\")")
        @Mapping(target = "combine", source = "combined")
        @Mapping(target = "onLoad", source = "load")
        @Mapping(target = "offLoad", source = "unload")
        @Mapping(target = "feeGenerated", source = "hasFees")
        @Mapping(target = "quotationStatus", source = "hasQuote")
        @Mapping(target = "shipMode", source = "distanceType")
        @Mapping(target = "arrivalTime", source = "bookingArrivalTime")
        @Mapping(target = "pickCovenantStatus", source = "bookingStatus")
        WaybillIndexOld convert(WaybillIndex source);

        @Mapping(target = "quotationStatus", ignore = true)
        void convert(@MappingTarget WaybillIndexOld index, WaybillRelation relation);
    }

    private static final Converter CONVERTER = Mappers.getMapper(Converter.class);

    @PostConstruct
    public void initialize() {
        this.diffs = redissonClient.getMapCache("PC::CACHE::INDEX::WAYBILL::DIFFS");
        this.ignores = redissonClient.getSet("PC::CACHE::INDEX::WAYBILL::DIFFS::IGNORES");
        this.page = redissonClient.getBucket("PC::CACHE::INDEX::WAYBILL::DIFFS::PAGE");
    }

    private void add(WaybillId waybillId, WaybillIndexOld oldIndex, WaybillIndexOld subIndex) {
        var diff = this.diffs.get(waybillId);
        if (null == diff) {
            diff = new ArrayList<>();
        }
        diff.add(new Diff(oldIndex, subIndex));
        this.diffs.fastPut(waybillId, diff);
    }

    public void diff(WaybillId waybillId) {
        if (ignores.contains(waybillId)) {
            logger.warn("Waybill index diff ignored: {}", waybillId);
            return;
        }

        reindexService.reindex(waybillId);
        var oldIndexes = indexOldRepository.list(waybillId);
        var newIndex = indexRepository.ofId(waybillId);
        if (oldIndexes.size() != newIndex.getRelations().size()) {
            logger.error("Waybill relations size no match: {} {}", oldIndexes, newIndex);
            return;
        }


        var relationMap = newIndex.getRelations()
                .stream()
                .collect(Collectors.toMap(WaybillRelation::getTenantId, i -> i));

        oldIndexes.forEach(oldIndex -> {
            var subIndex = CONVERTER.convert(newIndex);
            var relation = relationMap.get(oldIndex.getTenantId());
            if (null != relation) {
                CONVERTER.convert(subIndex, relation);
            }
            if (!oldIndex.equals(subIndex)) {
                logger.error("Waybill index no match: {}", waybillId);
                logger.info("Waybill index old: {}", JSON.toJSON(oldIndex));
                logger.info("Waybill index sub: {}", JSON.toJSON(subIndex));
                this.add(waybillId, oldIndex, subIndex);
            }
        });
    }

    private void diff(WaybillIndexOld index) {
        var waybillId = new WaybillId(index.getWaybillId());
        try {
            this.diff(waybillId);
        } catch (Exception e) {
            logger.error("Waybill index diff error: {}", index, e);
            this.ignores.add(waybillId);
        }
    }

    public void diffAll() {
        // 对比新旧索引是否相同
        var page = null != this.page.get() ? this.page.get() : 1;
        var size = 10;
        var completedSize = 0;
        logger.info("Waybill indexes diff start: {}", page);
        var oldIndexes = indexOldRepository.list(page++, size);
        while (!oldIndexes.getContent().isEmpty()) {
            oldIndexes.forEach(this::diff);
            completedSize = page * size + oldIndexes.getContent().size();
            logger.info("Waybill index diff size: {}", completedSize);
            this.page.set(page);
            oldIndexes = indexOldRepository.list(page++, size);
        }
    }

    public Map<Long, List<Diff>> diffs() {
        var diffs = new HashMap<Long, List<Diff>>();
        this.diffs.forEach(((key, value) -> diffs.put(key.getId(), value)));
        return diffs;
    }

    public Set<Long> ignores() {
        return this.ignores.stream()
                .map(WaybillId::getId)
                .collect(Collectors.toSet());
    }
}
